_.extend.set   F
last analyzed

Complexity

Conditions 16
Paths 1301

Size

Total Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
c 1
b 0
f 0
nc 1301
nop 3
dl 0
loc 84
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like _.extend.set often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import $ from 'jquery';
0 ignored issues
show
introduced by
Definition for rule 'keyword-spacing' was not found
Loading history...
2
import _ from 'underscore';
3
import {
4
  Backbone,
5
  urlError,
6
  wrapError,
7
  addUnderscoreMethods
8
} from './core.js';
9
import {
10
  Events
11
} from './events.js';
12
13
// Backbone.Model
14
// --------------
15
16
// Backbone **Models** are the basic data object in the framework --
17
// frequently representing a row in a table in a database on your server.
18
// A discrete chunk of data and a bunch of useful, related methods for
19
// performing computations and transformations on that data.
20
21
// Create a new model with the specified attributes. A client id (`cid`)
22
// is automatically generated and assigned for you.
23
var Model = function (attributes, options) {
24
  var attrs = attributes || {};
25
  options = options || {};
26
  this.preinitialize.apply(this, arguments);
27
  this.cid = _.uniqueId(this.cidPrefix);
28
  this.attributes = {};
29
  if (options.collection) {
30
    this.collection = options.collection;
31
  }
32
  if (options.parse) {
33
    attrs = this.parse(attrs, options) || {};
34
  }
35
  var defaults = _.result(this, 'defaults');
36
  attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
37
  this.set(attrs, options);
38
  this.changed = {};
39
  this.initialize.apply(this, arguments);
40
};
41
42
// Attach all inheritable methods to the Model prototype.
43
_.extend(Model.prototype, Events, {
44
45
  // A hash of attributes whose current and previous value differ.
46
  changed: null,
47
48
  // The value returned during the last failed validation.
49
  validationError: null,
50
51
  // The default name for the JSON `id` attribute is `"id"`. MongoDB and
52
  // CouchDB users may want to set this to `"_id"`.
53
  idAttribute: 'id',
54
55
  // The prefix is used to create the client id which is used to identify models locally.
56
  // You may want to override this if you're experiencing name clashes with model ids.
57
  cidPrefix: 'c',
58
59
  // preinitialize is an empty function by default. You can override it with a function
60
  // or object.  preinitialize will run before any instantiation logic is run in the Model.
61
  preinitialize: function () {},
62
63
  // Initialize is an empty function by default. Override it with your own
64
  // initialization logic.
65
  initialize: function () {},
66
67
  // Return a copy of the model's `attributes` object.
68
  toJSON: function (options) {
0 ignored issues
show
Unused Code introduced by
The parameter options is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
69
    return _.clone(this.attributes);
70
  },
71
72
  // Proxy `Backbone.sync` by default -- but override this if you need
73
  // custom syncing semantics for *this* particular model.
74
  sync: function () {
75
    return Backbone.sync.apply(this, arguments);
76
  },
77
78
  // Get the value of an attribute.
79
  get: function (attr) {
80
    return this.attributes[attr];
81
  },
82
83
  // Get the HTML-escaped value of an attribute.
84
  escape: function (attr) {
85
    return _.escape(this.get(attr));
86
  },
87
88
  // Returns `true` if the attribute contains a value that is not null
89
  // or undefined.
90
  has: function (attr) {
91
    return this.get(attr) != null;
92
  },
93
94
  // Special-cased proxy to underscore's `_.matches` method.
95
  matches: function (attrs) {
96
    return !!_.iteratee(attrs, this)(this.attributes);
97
  },
98
99
  // Set a hash of model attributes on the object, firing `"change"`. This is
100
  // the core primitive operation of a model, updating the data and notifying
101
  // anyone who needs to know about the change in state. The heart of the beast.
102
  set: function (key, val, options) {
103
    if (key == null) {
104
      return this;
105
    }
106
107
    // Handle both `"key", value` and `{key: value}` -style arguments.
108
    var attrs;
109
    if (typeof key === 'object') {
110
      attrs = key;
111
      options = val;
112
    } else {
113
      (attrs = {})[key] = val;
114
    }
115
116
    options = options || {};
117
118
    // Run validation.
119
    if (!this._validate(attrs, options)) {
120
      return false;
121
    }
122
123
    // Extract attributes and options.
124
    var unset = options.unset;
125
    var silent = options.silent;
126
    var changes = [];
127
    var changing = this._changing;
128
    this._changing = true;
129
130
    if (!changing) {
131
      this._previousAttributes = _.clone(this.attributes);
132
      this.changed = {};
133
    }
134
135
    var current = this.attributes;
136
    var changed = this.changed;
137
    var prev = this._previousAttributes;
138
139
    // For each `set` attribute, update or delete the current value.
140
    for (var attr in attrs) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
141
      val = attrs[attr];
142
      if (!_.isEqual(current[attr], val)) {
143
        changes.push(attr);
144
      }
145
      if (!_.isEqual(prev[attr], val)) {
146
        changed[attr] = val;
147
      } else {
148
        delete changed[attr];
149
      }
150
      unset ? delete current[attr] : current[attr] = val;
0 ignored issues
show
introduced by
Expected an assignment or function call and instead saw an expression.
Loading history...
151
    }
152
153
    // Update the `id`.
154
    if (this.idAttribute in attrs) {
155
      this.id = this.get(this.idAttribute);
156
    }
157
158
    // Trigger all relevant attribute changes.
159
    if (!silent) {
160
      if (changes.length) {
161
        this._pending = options;
162
      }
163
      for (var i = 0; i < changes.length; i++) {
164
        this.trigger('change:' + changes[i], this, current[
165
            changes[i]],
166
          options);
167
      }
168
    }
169
170
    // You might be wondering why there's a `while` loop here. Changes can
171
    // be recursively nested within `"change"` events.
172
    if (changing) {
173
      return this;
174
    }
175
    if (!silent) {
176
      while (this._pending) {
177
        options = this._pending;
178
        this._pending = false;
179
        this.trigger('change', this, options);
180
      }
181
    }
182
    this._pending = false;
183
    this._changing = false;
184
    return this;
185
  },
186
187
  // Remove an attribute from the model, firing `"change"`. `unset` is a noop
188
  // if the attribute doesn't exist.
189
  unset: function (attr, options) {
190
    return this.set(attr, void 0, _.extend({}, options, {
191
      unset: true
192
    }));
193
  },
194
195
  // Clear all attributes on the model, firing `"change"`.
196
  clear: function (options) {
197
    var attrs = {};
198
    for (var key in this.attributes) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
199
      attrs[key] = void 0;
200
    }
201
    return this.set(attrs, _.extend({}, options, {
202
      unset: true
203
    }));
204
  },
205
206
  // Determine if the model has changed since the last `"change"` event.
207
  // If you specify an attribute name, determine if that attribute has changed.
208
  hasChanged: function (attr) {
209
    if (attr == null) {
210
      return !_.isEmpty(this.changed);
211
    }
212
    return _.has(this.changed, attr);
213
  },
214
215
  // Return an object containing all the attributes that have changed, or
216
  // false if there are no changed attributes. Useful for determining what
217
  // parts of a view need to be updated and/or what attributes need to be
218
  // persisted to the server. Unset attributes will be set to undefined.
219
  // You can also pass an attributes object to diff against the model,
220
  // determining if there *would be* a change.
221
  changedAttributes: function (diff) {
222
    if (!diff) {
223
      return this.hasChanged() ? _.clone(this.changed) :
224
        false;
225
    }
226
    var old = this._changing ? this._previousAttributes : this.attributes;
227
    var changed = {};
228
    var hasChanged;
229
    for (var attr in diff) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
230
      var val = diff[attr];
231
      if (_.isEqual(old[attr], val)) {
232
        continue;
233
      }
234
      changed[attr] = val;
235
      hasChanged = true;
236
    }
237
    return hasChanged ? changed : false;
238
  },
239
240
  // Get the previous value of an attribute, recorded at the time the last
241
  // `"change"` event was fired.
242
  previous: function (attr) {
243
    if (attr == null || !this._previousAttributes) {
244
      return null;
245
    }
246
    return this._previousAttributes[attr];
247
  },
248
249
  // Get all of the attributes of the model at the time of the previous
250
  // `"change"` event.
251
  previousAttributes: function () {
252
    return _.clone(this._previousAttributes);
253
  },
254
255
  // Fetch the model from the server, merging the response with the model's
256
  // local attributes. Any changed attributes will trigger a "change" event.
257
  fetch: function (options) {
258
    options = _.extend({
259
      parse: true
260
    }, options);
261
    var model = this;
0 ignored issues
show
introduced by
Unexpected alias 'model' for 'this'.
Loading history...
262
    var success = options.success;
263
    options.success = function (resp) {
264
      var serverAttrs = options.parse ? model.parse(resp,
265
          options) :
266
        resp;
267
      if (!model.set(serverAttrs, options)) {
268
        return false;
269
      }
270
      if (success) {
271
        success.call(options.context, model, resp,
272
          options);
273
      }
274
      model.trigger('sync', model, resp, options);
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
275
    };
276
    wrapError(this, options);
277
    return this.sync('read', this, options);
278
  },
279
280
  // Set a hash of model attributes, and sync the model to the server.
281
  // If the server returns an attributes hash that differs, the model's
282
  // state will be `set` again.
283
  save: function (key, val, options) {
284
    // Handle both `"key", value` and `{key: value}` -style arguments.
285
    var attrs;
286
    if (key == null || typeof key === 'object') {
287
      attrs = key;
288
      options = val;
289
    } else {
290
      (attrs = {})[key] = val;
291
    }
292
293
    options = _.extend({
294
      validate: true,
295
      parse: true
296
    }, options);
297
    var wait = options.wait;
298
299
    // If we're not waiting and attributes exist, save acts as
300
    // `set(attr).save(null, opts)` with validation. Otherwise, check if
301
    // the model will be valid when the attributes, if any, are set.
302
    if (attrs && !wait) {
303
      if (!this.set(attrs, options)) {
304
        return false;
305
      }
306
    } else if (!this._validate(attrs, options)) {
307
      return false;
308
    }
309
310
    // After a successful server-side save, the client is (optionally)
311
    // updated with the server-side state.
312
    var model = this;
0 ignored issues
show
introduced by
Unexpected alias 'model' for 'this'.
Loading history...
313
    var success = options.success;
314
    var attributes = this.attributes;
315
    options.success = function (resp) {
316
      // Ensure attributes are restored during synchronous saves.
317
      model.attributes = attributes;
318
      var serverAttrs = options.parse ? model.parse(resp,
319
          options) :
320
        resp;
321
      if (wait) {
322
        serverAttrs = _.extend({}, attrs, serverAttrs);
323
      }
324
      if (serverAttrs && !model.set(serverAttrs, options)) {
325
        return false;
326
      }
327
      if (success) {
328
        success.call(options.context, model, resp,
329
          options);
330
      }
331
      model.trigger('sync', model, resp, options);
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
332
    };
333
    wrapError(this, options);
334
335
    // Set temporary attributes if `{wait: true}` to properly find new ids.
336
    if (attrs && wait) {
337
      this.attributes = _.extend({}, attributes,
338
        attrs);
339
    }
340
341
    var method = this.isNew() ? 'create' : (options.patch ?
0 ignored issues
show
introduced by
Do not nest ternary expressions
Loading history...
342
      'patch' :
343
      'update');
344
    if (method === 'patch' && !options.attrs) {
345
      options.attrs =
346
        attrs;
347
    }
348
    var xhr = this.sync(method, this, options);
349
350
    // Restore attributes.
351
    this.attributes = attributes;
352
353
    return xhr;
354
  },
355
356
  // Destroy this model on the server if it was already persisted.
357
  // Optimistically removes the model from its collection, if it has one.
358
  // If `wait: true` is passed, waits for the server to respond before removal.
359
  destroy: function (options) {
360
    options = options ? _.clone(options) : {};
361
    var model = this;
0 ignored issues
show
introduced by
Unexpected alias 'model' for 'this'.
Loading history...
362
    var success = options.success;
363
    var wait = options.wait;
364
365
    var destroy = function () {
366
      model.stopListening();
367
      model.trigger('destroy', model, model.collection, options);
368
    };
369
370
    options.success = function (resp) {
371
      if (wait) {
372
        destroy();
373
      }
374
      if (success) {
375
        success.call(options.context, model, resp,
376
          options);
377
      }
378
      if (!model.isNew()) {
379
        model.trigger('sync', model, resp,
380
          options);
381
      }
382
    };
383
384
    var xhr = false;
385
    if (this.isNew()) {
386
      _.defer(options.success);
387
    } else {
388
      wrapError(this, options);
389
      xhr = this.sync('delete', this, options);
390
    }
391
    if (!wait) {
392
      destroy();
393
    }
394
    return xhr;
395
  },
396
397
  // Default URL for the model's representation on the server -- if you're
398
  // using Backbone's restful methods, override this to change the endpoint
399
  // that will be called.
400
  url: function () {
401
    var base =
402
      _.result(this, 'urlRoot') ||
403
      _.result(this.collection, 'url') ||
404
      urlError();
405
    if (this.isNew()) {
406
      return base;
407
    }
408
    var id = this.get(this.idAttribute);
409
    return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
410
  },
411
412
  // **parse** converts a response into the hash of attributes to be `set` on
413
  // the model. The default implementation is just to pass the response along.
414
  parse: function (resp, options) {
0 ignored issues
show
Unused Code introduced by
The parameter options is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
415
    return resp;
416
  },
417
418
  // Create a new model with identical attributes to this one.
419
  clone: function () {
420
    return new this.constructor(this.attributes);
421
  },
422
423
  // A model is new if it has never been saved to the server, and lacks an id.
424
  isNew: function () {
425
    return !this.has(this.idAttribute);
426
  },
427
428
  // Check if the model is currently in a valid state.
429
  isValid: function (options) {
430
    return this._validate({}, _.extend({}, options, {
431
      validate: true
432
    }));
433
  },
434
435
  // Run validation against the next complete set of model attributes,
436
  // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
437
  _validate: function (attrs, options) {
438
    if (!options.validate || !this.validate) {
439
      return true;
440
    }
441
    attrs = _.extend({}, this.attributes, attrs);
442
    var error = this.validationError = this.validate(attrs,
443
        options) ||
444
      null;
445
    if (!error) {
446
      return true;
447
    }
448
    this.trigger('invalid', this, error, _.extend(options, {
449
      validationError: error
450
    }));
451
    return false;
452
  }
453
454
});
455
456
// Underscore methods that we want to implement on the Model, mapped to the
457
// number of arguments they take.
458
var modelMethods = {
459
  keys: 1,
460
  values: 1,
461
  pairs: 1,
462
  invert: 1,
463
  pick: 0,
464
  omit: 0,
465
  chain: 1,
466
  isEmpty: 1
467
};
468
469
// Mix in each Underscore method as a proxy to `Model#attributes`.
470
addUnderscoreMethods(Model, modelMethods, 'attributes');
471
472
export {
473
  Model
474
};
475